Estrazione dei dati.¶

Data download dei dati: 10-12-2024

Data ultimo upgrade dei dati: 22-11-2024

Sito origine dei dati: data.wa.gov

Link origine dei dati: https://catalog.data.gov/dataset/electric-vehicle-population-data

In [69]:
import polars as pl
from pathlib import Path

data_dir = Path('DATA')
data_file = ".."/ data_dir / "Electric_Vehicle_Population_Data.csv"

data = pl.read_csv(data_file)

#Breve visualizzazione dei dati
data.head()
data.describe()
Out[69]:
shape: (9, 18)
statisticVIN (1-10)CountyCityStatePostal CodeModel YearMakeModelElectric Vehicle TypeClean Alternative Fuel Vehicle (CAFV) EligibilityElectric RangeBase MSRPLegislative DistrictDOL Vehicle IDVehicle LocationElectric Utility2020 Census Tract
strstrstrstrstrf64f64strstrstrstrf64f64f64f64strstrf64
"count""216772""216767""216767""216772"216767.0216772.0"216772""216772""216772""216772"216753.0216753.0216321.0216772.0"216761""216767"216767.0
"null_count""0""5""5""0"5.00.0"0""0""0""0"19.019.0451.00.0"11""5"5.0
"mean"nullnullnullnull98179.7507142021.129039nullnullnullnull49.428386870.98704528.9201142.3045e8nullnull5.2982e10
"std"nullnullnullnull2458.3203232.983918nullnullnullnull86.2245117544.67159214.9079347.0450e7nullnull1.5147e9
"min""1C4JJXN60P""Ada""Aberdeen""AE"1731.01999.0"ACURA""330E""Battery Electric Vehicle (BEV)""Clean Alternative Fuel Vehicle…0.00.01.04385.0"POINT (-100.50078 31.4168)""AVISTA CORP"1.0010e9
"25%"nullnullnullnull98052.02020.0nullnullnullnull0.00.017.01.96232816e8nullnull5.3033e10
"50%"nullnullnullnull98125.02022.0nullnullnullnull0.00.032.02.44032309e8nullnull5.3033e10
"75%"nullnullnullnull98374.02023.0nullnullnullnull42.00.042.02.64906967e8nullnull5.3053e10
"max""ZHWUC1ZM1R""Yuba""Zillah""WY"99577.02025.0"WHEEGO ELECTRIC CARS""ZDX""Plug-in Hybrid Electric Vehicl…"Not eligible due to low batter…337.0845000.049.04.79254772e8"POINT (-98.72277 29.44539)""PUGET SOUND ENERGY INC||PUD NO…5.6021e10

Manipolazione dei dati¶

A seguito di un'analisi preliminare si nota che alcune colonne non risultano utili per l'analisi.

Lista colonne da eliminare¶

-VIN (1-10) -Postal Code -Base MSRP -Legislative District -DOL Vehicle ID -Electric Utility -2020 Census Tract -Clean Alternative Fuel Vehicle (CAFV) Eligibility

Lista colonne che verranno utilizzate con la relativa descrizione¶

La descrizione è in lingua inglese perchè è la descrizione data dal sito: data.gov

  • Country: This is the geographic region of a state that a vehicle's owner is listed to reside within. Vehicles registered in Washington state may be located in other states.
  • City: The city in which the registered owner resides.
  • State: This is the geographic region of the country associated with the record. These addresses may be located in other states.
  • Model Year: The model year of the vehicle.
  • Make: The manufacturer of the vehicle.
  • Model: The model of the vehicle.
  • Electric Vehicle Type: This distinguishes the vehicle as all electric or a plug-in hybrid.
  • Electric Range: Describes how far a vehicle can travel purely on its electric charge.
  • Vehicle Location: The center of the ZIP Code for the registered vehicle.
  • Base MSRP: This is the lowest Manufacturer's Suggested Retail Price (MSRP) for any trim level of the model in question.

Nota Bene I dato arrivano al 2025 a causa di alcuni preordini.

In [70]:
data = data.select(pl.exclude(['VIN (1-10)','Postal Code','Legislative District','DOL Vehicle ID','Electric Utility','2020 Census Tract','Clean Alternative Fuel Vehicle (CAFV) Eligibility']))
data.describe()
Out[70]:
shape: (9, 11)
statisticCountyCityStateModel YearMakeModelElectric Vehicle TypeElectric RangeBase MSRPVehicle Location
strstrstrstrf64strstrstrf64f64str
"count""216767""216767""216772"216772.0"216772""216772""216772"216753.0216753.0"216761"
"null_count""5""5""0"0.0"0""0""0"19.019.0"11"
"mean"nullnullnull2021.129039nullnullnull49.428386870.987045null
"std"nullnullnull2.983918nullnullnull86.2245117544.671592null
"min""Ada""Aberdeen""AE"1999.0"ACURA""330E""Battery Electric Vehicle (BEV)"0.00.0"POINT (-100.50078 31.4168)"
"25%"nullnullnull2020.0nullnullnull0.00.0null
"50%"nullnullnull2022.0nullnullnull0.00.0null
"75%"nullnullnull2023.0nullnullnull42.00.0null
"max""Yuba""Zillah""WY"2025.0"WHEEGO ELECTRIC CARS""ZDX""Plug-in Hybrid Electric Vehicl…337.0845000.0"POINT (-98.72277 29.44539)"
In [71]:
data = data.drop_nulls()

Cambiamo la denominazione della colonna Electric Vehicle Type, per semplificare la visualizzazione delle colonna.

In [72]:
data = data.with_columns(
    pl.col('Electric Vehicle Type').replace({
        'Plug-in Hybrid Electric Vehicle (PHEV)': 'PHEV',
        'Battery Electric Vehicle (BEV)': 'BEV'
    })
)

Obiettivi dell'analisi¶

Gli obiettivi sono quelli di eseguire un'analisi completa sul dataset cercando di mostrare graficamente le informazioni che riguardano la vendita di auto elettriche, in base a determinate categorie.

Per semplicità quando si analizzano entrambe le tipologie di vetture verrà usato il termine 'auto', invece se si fa riferimento solo ad una categoria delle due verranno usati i termini 'BEV' o 'PHEV'.

Vendita annuale di auto BEV/PHEV¶

In [73]:
(
    data
    .group_by('Model Year')
    .agg(
        tot_per_year = pl.col('Model Year').count()
    )
    .sort(pl.col('Model Year'))
)
Out[73]:
shape: (21, 2)
Model Yeartot_per_year
i64u32
19992
20007
20022
20031
200823
……
202120074
202228592
202360292
202440102
20251428

Si nota come nei primi anni il numero di auto vendute è molto basso, quindi per semplicità si prenderanno i dati a partire dal 2011.

In [74]:
data = (
    data
    .filter(pl.col('Model Year')>2010)
)
# salvataggio dei dati nel file data.csv 
data.write_csv( ".."/ data_dir / "data.csv")

Vendita di auto per marca¶

In [75]:
(
    data
    .group_by('Make')
    .agg(
        Vendita_per_marca = pl.col('Make').count()
    )
    .sort('Vendita_per_marca', descending = True)
)
Out[75]:
shape: (42, 2)
MakeVendita_per_marca
stru32
"TESLA"93883
"CHEVROLET"15862
"NISSAN"15011
"FORD"11477
"KIA"10089
……
"BENTLEY"5
"AZURE DYNAMICS"4
"ROLLS-ROYCE"3
"RAM"2
"VINFAST"1

Si nota che alcuni marchi hanno un numero di vendite quasi pari a 0, quindi guardo la percentuale totale di auto vendute per ogni produttore.

Per semplicità di analisi grafica, si vedranno solo le auto che hanno una percentuale di auto vendute nel dataset inferiore al 0.5%.

In [76]:
import altair as alt

data_test = (
        data
        .group_by('Make')
        .agg(
            Vendita_per_marca = (pl.col('Make').count() / len(data)*100).round(3)
        )
        .filter(pl.col('Vendita_per_marca') < .5)
    )
    
base = (
    alt.Chart(data_test)
    .encode(
        x='Vendita_per_marca:Q',
        y=alt.Y('Make:N', sort='-x'),
        color=alt.Color('Vendita_per_marca:Q', scale=alt.Scale(scheme='oranges'))
    )
)

base.mark_bar()
Out[76]:

Dal grafico notiamo come ci sia una differenza signficativa tra un gruppo di auto e un altro. Quindi scelgo come soglia di esclusione dall'analisi per marca il numero 0.25%.

In [77]:
(
    data
    .group_by('Make')
    .agg(
        Vendita_per_marca = (pl.col('Make').count() / len(data)*100).round(3)
    )
    .filter(pl.col('Vendita_per_marca') < .25)
    .sort('Vendita_per_marca', descending = True)
)
Out[77]:
shape: (16, 2)
MakeVendita_per_marca
strf64
"LINCOLN"0.148
"LUCID"0.146
"GENESIS"0.137
"SMART"0.113
"JAGUAR"0.109
……
"AZURE DYNAMICS"0.002
"TH!NK"0.002
"ROLLS-ROYCE"0.001
"RAM"0.001
"VINFAST"0.0

Vendita di auto per contea¶

In [78]:
Total_County = (
    data
    .group_by('County')
    .agg(
        Total = pl.col('County').count()
    )
    .sort('Total', descending= True)
)

Total_County
Out[78]:
shape: (205, 2)
CountyTotal
stru32
"King"110122
"Snohomish"26195
"Pierce"17392
"Clark"12937
"Thurston"7936
……
"Pettis"1
"Churchill"1
"Maui"1
"El Dorado"1
"New York"1

Risulta utile verificare la percentuale di auto immatricolate in base alla contea, così da avere un'idea più chiara di come si distribuiscono le auto rispetto al territorio.

In [79]:
num_car = len(data)

perc_county = (
    data
    .group_by('County')
    .agg(
        Total = (pl.col('County').count()/num_car*100).round(3)
    )
    .sort('Total', descending= True)
)

perc_county
Out[79]:
shape: (205, 2)
CountyTotal
strf64
"King"50.822
"Snohomish"12.089
"Pierce"8.026
"Clark"5.97
"Thurston"3.662
……
"Geary"0.0
"Chesterfield"0.0
"Cook"0.0
"Palm Beach"0.0
"Sussex"0.0

Analizzando la combinazione tra contea e numero di auto vendute per contea, si nota come ci siano enormi differenze tra le contee. Si nota come le contee vicine a Seattle e Portland(si intendono le contee confinanti con lo stato dell'Oregon) abbiano un numero di auto vendute significativo rispetto alle altre contee.

A causa di questo, in seguito si andrà ad analizzare questo dato in base alla localizzazione geografica e non alla contea.

Vendita di auto in base alla localizzazione¶

Per eseguire questa analisi, verifichiamo i dati che sono presenti nella colonna Vehicle Location.

Questi dati poi verranno usati per creare una mappa 3d in cui si vede dove sono state immatricolate le auto nello stato di Washington.

In [80]:
import re
import pandas as pd

data_point = (
    data
    .select(pl.col('Vehicle Location'))
)

#Si nota che è di tipo str
print(f'Tipologia del dato :{data_point[0]}')

#Creiamo una lista contenente tuple  ('longitue','latitude'): (float, float)
coord_list = []
for row in data_point.rows():
    if row[0] is not None:
        numb = re.findall(r'-?\d+\.\d+|-?\d+', row[0])
        coord = (float(numb[0]), float(numb[1]))
        coord_list.append(coord)

#verifichiamo la lunghezza della lista
print(f'Lunghezza della lista coord_list: {len(coord_list)}')

#verifichiamo i primi 5 elementi di coord_list

print('Tipologia e dato di coord_list')
for i in range(5):
    print(f'Tipo: {type(coord_list[i])} Dato:{coord_list[i]}')
Tipologia del dato :shape: (1, 1)
┌─────────────────────────────┐
│ Vehicle Location            │
│ ---                         │
│ str                         │
╞═════════════════════════════╡
│ POINT (-122.36498 47.72238) │
└─────────────────────────────┘
Lunghezza della lista coord_list: 216683
Tipologia e dato di coord_list
Tipo: <class 'tuple'> Dato:(-122.36498, 47.72238)
Tipo: <class 'tuple'> Dato:(-122.30207, 47.64085)
Tipo: <class 'tuple'> Dato:(-122.54729, 47.42602)
Tipo: <class 'tuple'> Dato:(-122.89166, 47.03956)
Tipo: <class 'tuple'> Dato:(-122.87741, 47.05997)

Vendita di auto in base a produttore e anno¶

In [81]:
(
    data
    .group_by('Make', 'Model Year')
    .agg(
        Vendita_per_marca = pl.col('Make').count()
    )
)
Out[81]:
shape: (267, 3)
MakeModel YearVendita_per_marca
stri64u32
"TESLA"20151016
"VOLKSWAGEN"2024497
"CADILLAC"202362
"PORSCHE"2023192
"VOLVO"2020212
………
"TESLA"2013723
"TOYOTA"2014227
"HONDA"2024684
"MITSUBISHI"202295
"CHEVROLET"2013761

Vendita di auto in base a tipologia di motore e produttore¶

In [82]:
(
    data
    .group_by('Make', 'Electric Vehicle Type')
    .agg(
        Engine = pl.col('Electric Vehicle Type').count()
    )
)
Out[82]:
shape: (60, 3)
MakeElectric Vehicle TypeEngine
strstru32
"PORSCHE""PHEV"543
"BMW""BEV"3387
"TOYOTA""PHEV"7424
"BENTLEY""PHEV"5
"LEXUS""PHEV"443
………
"DODGE""PHEV"701
"LAND ROVER""PHEV"90
"TH!NK""BEV"5
"HONDA""BEV"691
"VOLVO""PHEV"3557

Ventita annuale di auto in base al modello e marca¶

Per semplificare il layout del grafico a seguito di un'analisi è stato deciso di eliminare i modelli dei produttori che hanno venduto meno di 15 auto all'anno.

In [83]:
(
    data
    .group_by('Make', 'Model', 'Model Year')
    .agg(
        Total = pl.col('Model').count()
    )
    .filter(pl.col('Total') >= 15)
    .sort([pl.col('Model Year'),pl.col('Total')], descending=False)
)
Out[83]:
shape: (432, 4)
MakeModelModel YearTotal
strstri64u32
"CHEVROLET""VOLT"201173
"NISSAN""LEAF"2011610
"MITSUBISHI""I-MIEV"201240
"TESLA""MODEL S"2012128
"TOYOTA""PRIUS PLUG-IN"2012368
…………
"VOLVO""XC60"202595
"BMW""IX"2025112
"NISSAN""LEAF"2025139
"BMW""X5"2025189
"RIVIAN""R1S"2025412

Analisi su prestazioni e costi delle auto¶

Analisi esplorativa su prestazioni e costi.

Si vede inizialmente se tutti i dati del dataset dispongono delle informazioni su prestazioni e costi

In [84]:
data.describe()
Out[84]:
shape: (9, 11)
statisticCountyCityStateModel YearMakeModelElectric Vehicle TypeElectric RangeBase MSRPVehicle Location
strstrstrstrf64strstrstrf64f64str
"count""216683""216683""216683"216683.0"216683""216683""216683"216683.0216683.0"216683"
"null_count""0""0""0"0.0"0""0""0"0.00.0"0"
"mean"nullnullnull2021.132636nullnullnull49.391009849.617044null
"std"nullnullnull2.974965nullnullnull86.1983177399.818083null
"min""Ada""Aberdeen""AK"2011.0"ACURA""330E""BEV"0.00.0"POINT (-100.50078 31.4168)"
"25%"nullnullnull2020.0nullnullnull0.00.0null
"50%"nullnullnull2022.0nullnullnull0.00.0null
"75%"nullnullnull2023.0nullnullnull42.00.0null
"max""Yuba""Zillah""WY"2025.0"VOLVO""ZDX""PHEV"337.0845000.0"POINT (-98.72277 29.44539)"

Le colonne interessanti da analizzare sono:

  • Electric Vehicle Type
  • Electric Range
  • Base MSRP

Analisi Electric Vehicle Type¶

In [85]:
data_ER = (
    data
    .filter(pl.col('Electric Range') > 0)
)

data_ER.describe()
Out[85]:
shape: (9, 11)
statisticCountyCityStateModel YearMakeModelElectric Vehicle TypeElectric RangeBase MSRPVehicle Location
strstrstrstrf64strstrstrf64f64str
"count""92587""92587""92587"92587.0"92587""92587""92587"92587.092587.0"92587"
"null_count""0""0""0"0.0"0""0""0"0.00.0"0"
"mean"nullnullnull2018.869453nullnullnull115.5906551988.373854null
"std"nullnullnull3.240357nullnullnull98.6756111219.89275null
"min""Adams""Aberdeen""AL"2011.0"ALFA ROMEO""330E""BEV"6.00.0"POINT (-100.50078 31.4168)"
"25%"nullnullnull2017.0nullnullnull30.00.0null
"50%"nullnullnull2019.0nullnullnull73.00.0null
"75%"nullnullnull2021.0nullnullnull215.00.0null
"max""Yakima""Zillah""WI"2025.0"VOLVO""XM""PHEV"337.0845000.0"POINT (-98.52212 29.61445)"

Si nota che solo 92587 solo le righe che hanno il dato Electric Range maggiore di 0. Per eseguire l'analisi si consideranno solo i record che hanno dati positivi.

Distribuzione Electric Range per tipologia di motore¶

In [86]:
data_ER_EN = (
    data
    .select('Electric Vehicle Type', 'Electric Range')
    .filter(pl.col('Electric Range') > 0)
)

data_ER_EN = (
    data_ER_EN
    .group_by('Electric Vehicle Type', 'Electric Range')
    .agg(
        Count = pl.col('Electric Range').count()
    )
    .sort(pl.col('Electric Range'))
)

data_ER_EN = data_ER_EN.with_columns(
        pl.col('Electric Vehicle Type').replace({
            'Plug-in Hybrid Electric Vehicle (PHEV)': 'PHEV',
            'Battery Electric Vehicle (BEV)': 'BEV'
        })
    )
In [87]:
base = (
    alt.Chart(data_ER_EN)
    .encode(
        x = ('Electric Range:Q'),
        y = alt.Y('Count:Q'),
        color= alt.Color('Electric Vehicle Type:N', scale=alt.Scale(scheme='oranges'))
    )
)

base.mark_bar(cornerRadiusTopLeft=3, cornerRadiusTopRight=3)
Out[87]:

Come facilmente ipotizzabile, le auto di tipologia PHEV hanno un'autonomia in solo elettrico minore rispetto alle auto BEV.

Autonomia in elettrico rispetto al produttore¶

Si vuole vedere com'è distribuita l'autonomia delle auto prodotte in base al venditore.

In [88]:
data_range_prod = (
    data
    .select('Make', 'Electric Range', 'Electric Vehicle Type')
    .filter(pl.col('Electric Range') > 0 )
)
data_range_prod = data_range_prod.with_columns(
        pl.col('Electric Vehicle Type').replace({
            'Plug-in Hybrid Electric Vehicle (PHEV)': 'PHEV',
            'Battery Electric Vehicle (BEV)': 'BEV'
        })
    )
print(data_range_prod)
shape: (92_587, 3)
┌───────────┬────────────────┬───────────────────────┐
│ Make      ┆ Electric Range ┆ Electric Vehicle Type │
│ ---       ┆ ---            ┆ ---                   │
│ str       ┆ i64            ┆ str                   │
╞═══════════╪════════════════╪═══════════════════════╡
│ NISSAN    ┆ 75             ┆ BEV                   │
│ TESLA     ┆ 270            ┆ BEV                   │
│ TOYOTA    ┆ 25             ┆ PHEV                  │
│ FORD      ┆ 19             ┆ PHEV                  │
│ TESLA     ┆ 266            ┆ BEV                   │
│ …         ┆ …              ┆ …                     │
│ CHRYSLER  ┆ 32             ┆ PHEV                  │
│ CHEVROLET ┆ 259            ┆ BEV                   │
│ CHEVROLET ┆ 38             ┆ PHEV                  │
│ KIA       ┆ 33             ┆ PHEV                  │
│ CHEVROLET ┆ 38             ┆ PHEV                  │
└───────────┴────────────────┴───────────────────────┘

L'obiettivo è vedere tramite dei grafici a violino quante auto sono state vendute rispetto al'autonomia del motore elettrico per ogni marca.

Per farlo bisogna creare una tabelle in cui si contano il numero di auto che ha la stessa autonomia in elettrico per ogni produttore.

In [89]:
data_violin_graph=(
    data_range_prod
    .group_by('Make','Electric Range', )
    .agg(
        Count = pl.col('Electric Range').count()
    )
    .sort('Make')
)

print(data_violin_graph)
shape: (178, 3)
┌────────────┬────────────────┬───────┐
│ Make       ┆ Electric Range ┆ Count │
│ ---        ┆ ---            ┆ ---   │
│ str        ┆ i64            ┆ u32   │
╞════════════╪════════════════╪═══════╡
│ ALFA ROMEO ┆ 33             ┆ 90    │
│ AUDI       ┆ 16             ┆ 567   │
│ AUDI       ┆ 23             ┆ 510   │
│ AUDI       ┆ 22             ┆ 52    │
│ AUDI       ┆ 222            ┆ 136   │
│ …          ┆ …              ┆ …     │
│ VOLVO      ┆ 22             ┆ 101   │
│ VOLVO      ┆ 41             ┆ 162   │
│ VOLVO      ┆ 13             ┆ 111   │
│ VOLVO      ┆ 14             ┆ 110   │
│ VOLVO      ┆ 21             ┆ 30    │
└────────────┴────────────────┴───────┘

Per creare dei grafici con una buona quantità di dati eseguaiamo un controllo sul numero di dati disponibili. Per farlo verifichiamo quali sono le auto al di sotto di un certo range, scegliamo il 5%.

Creaiamo un grafico per vedere quali marchi eliminare dall'analisi successiva.

In [90]:
data_test_violin = (
    data_range_prod
    .group_by('Make')
    .agg(
        Count = pl.col('Make').count()/data_range_prod.height*100
    )
    .filter(pl.col('Count') < 5)
    .sort(pl.col('Count'))
)

base = (
    alt.Chart(data_test_violin)
    .encode(
        x='Count:Q',
        y=alt.Y('Make:N', sort='-x'),
        color=alt.Color('Count:Q', scale=alt.Scale(scheme='oranges'))
    )
)

base.mark_bar()

#print(data_test_violin)
Out[90]:

Scegliamo di eliminare i produttori che hanno venduto meno di MERCEDES-BENZ.

Creiamo una lista con i produttori di auto che rispettano il parametro di percentuale di auto vendute al di sopra del 0.5%.

In [91]:
make_list = (
    data_range_prod
    .group_by('Make')
    .agg(
        Count = pl.col('Make').count()/data_range_prod.height*100
    )
    .filter(pl.col('Count') > 0.5)
)


make_list = make_list['Make'].unique().sort().to_list()

make_list
Out[91]:
['AUDI',
 'BMW',
 'CHEVROLET',
 'CHRYSLER',
 'DODGE',
 'FIAT',
 'FORD',
 'HONDA',
 'HYUNDAI',
 'JEEP',
 'KIA',
 'MAZDA',
 'MERCEDES-BENZ',
 'MITSUBISHI',
 'NISSAN',
 'PORSCHE',
 'TESLA',
 'TOYOTA',
 'VOLKSWAGEN',
 'VOLVO']

Infine creaimo un grafico a violini, che ha l'obiettivo di vedere il numero di auto vendute per range dalla verie case automobilistiche.

Nell'asse delle X si vedranno il numero di auto vendute, nell'asse Y si vede il range, questo avviene per ogni produttore.

In [92]:
alt.data_transformers.disable_max_rows()

base = (
    alt.Chart(data_violin_graph)
    .encode(
        alt.X('Count:Q')
            .stack('center')
            .impute(None)
            .title(None)
            .axis(labels = False, values = [0], grid = False, ticks = True),
        alt.Y('Electric Range:Q'),    
        alt.Color('Make:N'),
        alt.Column('Make:N')
            .spacing(0)
            .header(titleOrient='bottom', labelOrient='bottom', labelPadding=0)
    )
    .configure_view(
        stroke = None
    )
)

base.mark_area(orient='horizontal')
Out[92]:

Con questa visualizzazione non si ha una visuale chiara dei dati. Proviamo con un tipo diverso di grafico.

Creiamo un grafico a linee in cui nell'asse delle X ci sono i chilometri di autonomia del motore elettrico, mentre nell'asse delle Y ci sono i produttori di auto.

In [93]:
bar = (
    alt.Chart(data_violin_graph)
    .mark_bar(cornerRadius=10, height=10)
    .encode(
        x = alt.X('min(Electric Range):Q').scale(domain=[-15, 350]).title('Electric Range'),
        x2 = 'max(Electric Range):Q',
        y = alt.Y('Make:N', sort = '-x').title(None),
    )
)

text_min = (
    alt.Chart(data_violin_graph)
    .mark_text(align='right', dx = -5)
    .encode(
        x = 'min(Electric Range):Q',
        y = alt.Y('Make:N'),
        text = 'min(Electric Range):Q'
    )
)

text_max = (
    alt.Chart(data_violin_graph)
    .mark_text(align='left', dx = 5)
    .encode(
        x = 'max(Electric Range):Q',
        y = alt.Y('Make:N'),
        text = 'max(Electric Range):Q'
    )
)

(bar + text_min + text_max).properties(
    title = alt.Title(text = 'Electric Range')
)
Out[93]:

Il grafico ottenuto è interessante, ma per motivi di chiarezza si andranno a selezionare i marchi che hanno una differenza di autonomia di motore elettrico (modello con minima autonomia e modello con massima autonomia superiore a 5).

Successivamente nella pagina streamlit, questo verrà affiancato da una tabella e da una spiegazione così da non perdere i dati.

In [94]:
filtered_data = (
    data_violin_graph
    .group_by("Make")
    .agg([
        pl.col("Electric Range").max().alias("max_Electric_Range"),
        pl.col("Electric Range").min().alias("min_Electric_Range"),
    ])
    .filter((pl.col("max_Electric_Range") - pl.col("min_Electric_Range")) < 5)
)

filter_list = filtered_data['Make'].unique().sort().to_list()

filter_list

data_violin_graph = (
    data_violin_graph
    .filter(pl.col('Make').is_in(list(set(make_list) - set(filter_list))))
)

bar = (
    alt.Chart(data_violin_graph)
    .mark_bar(cornerRadius=10, height=10)
    .encode(
        x = alt.X('min(Electric Range):Q').scale(domain=[-15, 350]).title('Electric Range'),
        x2 = 'max(Electric Range):Q',
        y = alt.Y('Make:N', sort = '-x').title(None),
        color=alt.value('orange')
    )
)

text_min = (
    alt.Chart(data_violin_graph)
    .mark_text(align='right', dx = -5)
    .encode(
        x = 'min(Electric Range):Q',
        y = alt.Y('Make:N'),
        text = 'min(Electric Range):Q'
    )
)

text_max = (
    alt.Chart(data_violin_graph)
    .mark_text(align='left', dx = 5)
    .encode(
        x = 'max(Electric Range):Q',
        y = alt.Y('Make:N'),
        text = 'max(Electric Range):Q'
    )
)

(bar + text_min + text_max).properties(
    title = alt.Title(text = 'Electric Range')
)
Out[94]:

Etichette utili all'analisi¶

Eseguiamo un analisi per quanto riguarda la tipologia del motore. Questa successivamente sarà molto utile perchè ci servirà per creare la visualizzazione sul sito streamlit.

L'obiettivo è quello di suddividere le auto per tipologia di motore, e verificare la media dell'autonomia suddivisa per categoria.

In [95]:
#Numero dati con cui eseguiamo l'analisi
data_label = (
    data
    .select('Electric Vehicle Type', 'Electric Range')
    .filter(pl.col('Electric Range') > 0)
)

data_label = data_label.with_columns(
        pl.col('Electric Vehicle Type').replace({
            'Plug-in Hybrid Electric Vehicle (PHEV)': 'PHEV',
            'Battery Electric Vehicle (BEV)': 'BEV'
        })
    )

data_label.height
data_label

data_mean = (
    data_label
    .mean()
    .select('Electric Range').item()
)

round(data_mean, 2)
Out[95]:
115.59

Ora dividiamo per tipologia di motore

In [96]:
data_label
Out[96]:
shape: (92_587, 2)
Electric Vehicle TypeElectric Range
stri64
"BEV"75
"BEV"270
"PHEV"25
"PHEV"19
"BEV"266
……
"PHEV"32
"BEV"259
"PHEV"38
"PHEV"33
"PHEV"38
In [97]:
data_mean_by_engine = (
    data_label
    .group_by('Electric Vehicle Type')
    .agg(
        Count = pl.col('Electric Range').count(),
        Mean = pl.col('Electric Range').mean()
    )
    .with_columns(
        pl.col('Mean').round(2).alias('Mean')  
    )
)

data_mean_by_engine

value = data_mean_by_engine.row(0)[1]

print(data_mean_by_engine)
shape: (2, 3)
┌───────────────────────┬───────┬────────┐
│ Electric Vehicle Type ┆ Count ┆ Mean   │
│ ---                   ┆ ---   ┆ ---    │
│ str                   ┆ u32   ┆ f64    │
╞═══════════════════════╪═══════╪════════╡
│ BEV                   ┆ 47026 ┆ 197.52 │
│ PHEV                  ┆ 45561 ┆ 31.03  │
└───────────────────────┴───────┴────────┘

Strip plot jitter¶

Risulta interessante ora vedere come sono distribuite le vendite in base all'autonomia. Inizialmente vediamo come si distribuisce a livello di campione, successivamente si va a distinguere per produttore.

In [98]:
data_strip_plot = (
    data
    .select('Make', 'Electric Range', 'Electric Vehicle Type')
    .filter(pl.col('Electric Range') > 0 )
)
data_strip_plot = data_strip_plot.with_columns(
    pl.col('Electric Vehicle Type').replace({
        'Plug-in Hybrid Electric Vehicle (PHEV)': 'PHEV',
        'Battery Electric Vehicle (BEV)': 'BEV'
    })
)

gaussian_jitter = alt.Chart(data_strip_plot).mark_circle(size = 8).encode(
    y = 'Electric Vehicle Type:N',
    x = 'Electric Range:Q',
    yOffset='jitter:Q',
    color=alt.Color('Electric Vehicle Type:N', scale=alt.Scale(scheme='oranges')).legend(None)
).transform_calculate(
    jitter = "sqrt(-2*log(random()))*cos(2*PI*random())"
)

uniform_jitter = gaussian_jitter.transform_calculate(
    jitter='random()'
).encode(
    alt.Y('Electric Vehicle Type:N').axis(None)
)

(gaussian_jitter | uniform_jitter).resolve_scale(yOffset='independent')
Out[98]:

Ora facciamo lo stesso grafico, per le marche che fanno parte del dataset del grafico in cui si vede la massima e la minima autonomia dei modelli venduti.

In [99]:
def make_strip_plot(data):
    data_range_prod = (
        data
        .select('Make', 'Electric Range', 'Electric Vehicle Type')
        .filter(pl.col('Electric Range') > 0 )
    )
    data_range_prod = data_range_prod.with_columns(
        pl.col('Electric Vehicle Type').replace({
            'Plug-in Hybrid Electric Vehicle (PHEV)': 'PHEV',
            'Battery Electric Vehicle (BEV)': 'BEV'
        })
    )

    range_graph = (
        data_range_prod
        .group_by('Make','Electric Range', )
        .agg(
            Count = pl.col('Electric Range').count()
        )
        .sort('Make')
    )

    make_list = (
        data_range_prod
        .group_by('Make')
        .agg(
            Count = pl.col('Make').count()/data_range_prod.height*100
        )
        .filter(pl.col('Count') > 0.5)
    )

    make_list = make_list['Make'].unique().sort().to_list()
    
    filtered_data = (
        range_graph
        .group_by("Make")
        .agg([
            pl.col("Electric Range").max().alias("Max Range"),
            pl.col("Electric Range").min().alias("Min Range"),
        ])
    )

    filtered_data = (
        filtered_data
        .filter((pl.col("Max Range") - pl.col("Min Range")) < 5)
    )

    filter_list = filtered_data['Make'].unique().sort().to_list()
    
    

    return list(set(make_list) - set(filter_list))

test = make_strip_plot(data)

data_jitter_plot = (
    data
    .select('Make', 'Electric Range', )
    .filter(pl.col('Electric Range') > 0)
    .filter(pl.col('Make').is_in(test))
)

gaussian_jitter = alt.Chart(data_jitter_plot).mark_circle(size = 8).encode(
    y = 'Make:N',
    x = 'Electric Range:Q',
    yOffset='jitter:Q',
    color=alt.Color('Make:N', scale=alt.Scale(scheme='oranges')).legend(None)
).transform_calculate(
    jitter = "sqrt(-2*log(random()))*cos(2*PI*random())"
)

(gaussian_jitter).resolve_scale(yOffset='independent')
Out[99]:

Visto così non risulta molto utile, si può optare per vedere questo grafico suddiviso per marchio

In [100]:
test_list = ['TESLA', 'HYUNDAI', 'KIA', 'TOYOTA']

test_grafico = (
    data
    .select('Make', 'Electric Range', 'Electric Vehicle Type')
    .filter(pl.col('Make').is_in(test_list))
    .filter(pl.col('Electric Range') > 0)
)

gaussian_jitter = alt.Chart(test_grafico).mark_circle(size = 8).encode(
    y = 'Make:N',
    x = 'Electric Range:Q',
    yOffset='jitter:Q',
    color=alt.Color('Electric Vehicle Type:N', scale=alt.Scale(scheme='purpleorange'))
).transform_calculate(
    jitter = "sqrt(-2*log(random()))*cos(2*PI*random())"
)

(gaussian_jitter).resolve_scale(yOffset='independent')
Out[100]:

ANALISI SUI PREZZI¶

Analisi sui prezzi delle auto.

In [101]:
data_prezzi = (
    data.filter(pl.col('Base MSRP') > 0)
)

data_prezzi.describe()
Out[101]:
shape: (9, 11)
statisticCountyCityStateModel YearMakeModelElectric Vehicle TypeElectric RangeBase MSRPVehicle Location
strstrstrstrf64strstrstrf64f64str
"count""3259""3259""3259"3259.0"3259""3259""3259"3259.03259.0"3259"
"null_count""0""0""0"0.0"0""0""0"0.00.0"0"
"mean"nullnullnull2015.717091nullnullnull119.62227756488.975146null
"std"nullnullnull2.39341nullnullnull89.97930322311.569047null
"min""Adams""Aberdeen""CA"2011.0"BMW""330E""BEV"12.031950.0"POINT (-115.20278 36.29143)"
"25%"nullnullnull2013.0nullnullnull17.039995.0null
"50%"nullnullnull2016.0nullnullnull93.059900.0null
"75%"nullnullnull2018.0nullnullnull208.069900.0null
"max""Yakima""Zillah""WA"2020.0"VOLVO""XC90""PHEV"265.0845000.0"POINT (-97.85322 30.57955)"

La prima cosa che si nota è che il numero di dati presenti cala in modo molto significativo. Vediamo come cambia la media della base di vendita anno per anno.

In [102]:
data_year_price = (
    data_prezzi
    .group_by('Model Year')
    .agg(
        Mean = pl.col('Base MSRP').mean(),
        Count = pl.col('Model Year').count()
    )
    .filter(pl.col('Count')>100)
    .sort('Model Year')
)

data_year_price
Out[102]:
shape: (7, 3)
Model YearMeanCount
i64f64u32
201263508.571429140
201369900.0723
201469900.0617
201632203.369272371
201739228.54250
201853920.394127647
201944561.356994479
In [103]:
alt.Chart(data_year_price).mark_line(point={'color': 'orange', 'size': 30}, color='orange').encode(
    x='Model Year:O',
    y='Mean:Q',
).properties(
    width = 500
)
Out[103]:

Risulta interessante vedere con uno scatter plot se la tipologia del motore incide sul prezzo.

A seguito di una piccola analisi, risulta molto utile eliminare i dati che hanno una valore di Base MSRP sopra i 120k dollari, altrimenti porta ad avere una visualizzazione dei dati non chiara.

In [104]:
data_scatter_price_range = (
    data_prezzi
    .select('Base MSRP', 'Electric Range', 'Electric Vehicle Type')
    .filter(pl.col('Electric Range') > 0)
    .filter(pl.col('Base MSRP') < 120000)
)
data_scatter_price_range
Out[104]:
shape: (3_248, 3)
Base MSRPElectric RangeElectric Vehicle Type
i64i64str
4410014"PHEV"
3195093"BEV"
69900208"BEV"
3195093"BEV"
59900265"BEV"
………
69900208"BEV"
69900208"BEV"
4560014"PHEV"
109000245"BEV"
3999532"PHEV"
In [106]:
alt.Chart(data_scatter_price_range).mark_point().encode(
    x='Electric Range:Q',
    y='Base MSRP:Q',
    color=alt.Color('Electric Vehicle Type:N', scale=alt.Scale(scheme='purpleorange')),
    size= 'count()'
)
Out[106]:

PARTE DI MIGLIORAMENTO¶

In questa parte aggiungo del codice per vedere se si riescono ad aggiungere altre cose interessanti alla dashboard.

Ora si vuole vedere la percentuale di auto per tipologia di motore che un determinare produttore ha venduto.

In [ ]:
data_HONDA = (
    data
    .filter(
        pl.col('Make') == 'HONDA'
    )
)
In [133]:
test = (
    data_HONDA
    #.select('Make')
    .group_by('Electric Vehicle Type')
    .agg(
        Tot_Engine = ((pl.col('Electric Vehicle Type').count())/data_HONDA.height).round(2)
    )
    .sort(pl.col('Electric Vehicle Type'))
)

test
Out[133]:
shape: (2, 2)
Electric Vehicle TypeTot_Engine
strf64
"BEV"0.44
"PHEV"0.56

Questo risultato lo aggiungo alla label del mini report.

Ora si vuole contare il numero di modelli venduti da un produttore suddivisi per tipologia di motore.

In [135]:
model_engine =(
    data_HONDA
    .select('Model', 'Electric Vehicle Type').unique()
)

(
    model_engine
    .group_by('Electric Vehicle Type')
    .agg(
        tot = pl.col('Electric Vehicle Type').count()
    )
    .sort(pl.col('Electric Vehicle Type'))
)
Out[135]:
shape: (2, 2)
Electric Vehicle Typetot
stru32
"BEV"2
"PHEV"2